/**
 **ji - A vi like code editor
 *
 * The ji module provides a vi like code editor for editing oj files.
 *
 * Ji has two modes, insert mode and command mode. The editor begins in
 * insert mode, indicated by a thin caret positioned between characters.
 * To switch from insert mode to command mode press [ESC]. Command mode
 * is indicated by a wide caret and the visibility of the command bar.
 */

/**
 **Command mode
 *
 * Switching back to insert mode
 *
 *.a    append text, after the cursor
 *.A    append text, after end of line
 *.i    insert text, before the cursor
 *.I    insert text, before first non-whitespace character
 *.o    open new line below cursor in Insert Mode
 *.O    open new line above cursor in Insert Mode  
 *
 */

define(
'oj/ji',
[
'oj/file',
'oj/element',
'oj/javascript'
],
function (file, element, javascript) {
    'use strict';
    
    
    /**
     **oj.main
     *
     * This module only works as a main application. A file will be opened
     * at start up with a path specified by the URL parameter f.
     *
     *. URL:     /?oj/ji&f=/file.js
     *. oj/term: oj/ji file.js
     *
     */
    
    if (oj.main !== 'oj/ji') {
        return;
    }
    
    var container = document.body,
        font    = {
            css   : 'normal normal normal 14pt Courier New',
            style : undefined,
            width : undefined,
            height: undefined
        },
        editel  = undefined,    // editarea element
        text    = 'X',          // holds text of editarea
        lines   = 0,
        command = {             // command bar elements
            el    : undefined,
            input : undefined,
            info  : undefined,
            mode  : false
        },
        underlay = {            // span of syntax highlighting
            el    : undefined,
            tmp   : undefined,
            els   : []
        },
        ruler = {               // span of ruler
            padding : 2,
            el    : undefined,
            tmp   : undefined,
            els   : []
        },
        cursor      = {         // cursor information
            el        : undefined,
            pos       : -1,
            indent    : undefined,
            offsetTop : 0
        },
        insertmode = true,        // what mode we are in
        editbuffer = '',          // buffer of keys pressed in edit mode
        tokens     = [],          // holds previously parsed tokens
        colours    = {
            token  : [            // syntax highlight colours
                '#999999',        //operator
                '#3a3a3a',        //space
                '#3a3a3a',        //linebreak
                '#00aa00',        //comment
                '#FB660A',        //cword
                '#FFFFFF',        //name
                '#0086FF',        //number
                '#0086FF',        //string
                '#555555',        //operator
                '#eeeeee'         //eof
            ],
            ruler       : '#222222',
            linenumber  : '#777777',
            lineborder  : '#555555',
            cmdbarbg    : '#eeeeee',
            background  : '#333333',
            cursorInsert: '#FFFFFF',
            cursorEdit  : '#AAAAAA'
        },
        editfile    = undefined,
        lastKeyType = 'keyup',
        a = 0, b = 0, c = 0,
        pos         = 0,
        precursor   = 0,
        indent      = 0,
        ntokens     = 0,
        i = 0, el = 0,
        ntext       = 0,
        token       = 0;
    
    
    function open( path ) {
        /*
         * Opens the File at path for editing.
         */
        
        editfile = file.open( path );
        
        editfile.read( function ( read ) {
            document.title = read.file.path;
            editel.value = read.file.content;
            editel.selectionStart = editel.selectionEnd = 0;
            refreshDisplay();
        } );
    };
    
    
    function save( callback ) {
        /*
         * Writes contents of editarea to File and flushes to server.
         */
        
        // can't save if no associated File
        if (typeof editfile === 'undefined') return;
        
        editfile.content = editel.value;
        
        i = 0;
        ntext = command.info.textContent;
        
        editfile.flush(function (flush) {
            if (flush.hasOwnProperty('multiparts')) {
                a = flush.multiparts;
                return;
            }
            if (flush.hasOwnProperty('multipart')) {
                command.info.textContent = '' + ++i + '/' + a + '  ' + ntext;
                return;
            }
            if (flush.hasOwnProperty('success')) {
                command.info.textContent = '-saved-  ' + ntext;
            } else if (flush.hasOwnProperty('error')) {
                command.info.textContent = flush.error + '  ' + ntext;
            }
            
            if (typeof callback === 'function') callback(flush);
        });
    };
    
    
    function scrollToCursor() {
        /*
         * Scrolls view so cursor is visible.
         */
        
        i = ( document.documentElement.scrollTop || document.body.scrollTop );
        precursor = window.innerHeight || document.documentElement.clientHeight;
        
        if ( cursor.offsetTop < i ) {
            i = cursor.offsetTop;
        } else if ( cursor.offsetTop + font.height > i + precursor ) {
            i = ( cursor.offsetTop + font.height ) - precursor;
        }
        
        document.documentElement.scrollTop = i;
        document.body.scrollTop = i;
    };
    
    
    function refreshCursor() {
        /*
         * Updates cursor position.
         */
        
        precursor = editel.value.substring(0, editel.selectionStart).replace(/[^\n]*\n/g, '\n');
        indent = precursor.replace(/\n/g, '').length;
        
        if (insertmode) cursor.indent = indent;
        
        cursor.offsetTop = ruler.el.offsetTop + precursor.replace(/[^\n]/g, '').length * font.height;
        cursor.pos = editel.selectionStart;
        cursor.el.style.left = String(ruler.el.offsetWidth + ruler.padding + (indent * font.width)) + 'px';
        cursor.el.style.top = String(cursor.offsetTop) + 'px';
        
        scrollToCursor();
            
        command.info.textContent = cursor.pos + ':' + String(editel.value.length);
        if (typeof editfile === 'object') {
            command.info.textContent += '  ' + editfile.path + '  ';
        } else {
            command.info.textContent += '  cmd  ';
        }
    };
    
    
    function refreshDisplay() {
        /*
         * Updates display.
         */
        
        
        // only update cursor position if changed
        if (cursor.pos !== editel.selectionStart) refreshCursor();
        
        // dont update syntax highlighting if text hasn't changed
        if (text === editel.value) return;
        
        text = editel.value;
        
        ntokens = javascript.tokens(text);
        
        // update the number of rows on the editarea and ruler count
        i = text.split('\n').length;
        while (i > editel.rows) {
            ruler.els[editel.rows++].style.display = 'inline';
        }
        while (i <= lines) {
            ruler.els[lines--].style.display = 'none';
        }
        while (lines < i) {
            ruler.els[lines++].style.display = 'inline';
        }
        container.style.height = editel.offsetHeight;
        
        
        // detach underlay from DOM while updating to avoid render updates
        container.removeChild(underlay.el);
        
        // update the syntax highlights
        for (i = 0; i < ntokens.length; i++) {
            token = ntokens[i];
            
            if (i < tokens.length && token.value === tokens[i].value) continue;
            
            if (i === underlay.els.length) {
                el = underlay.tmp.cloneNode(true);
                underlay.els.push(el);
                underlay.el.appendChild(el);
            } else {
                el = underlay.els[i];
                el.style.display = 'inline';
            }
            
            el.style.color = colours.token[token.type];
            
            if (token.type === 2) {
                el.textContent = token.value.replace(/\n/g, '$\n');
            } else if (token.type === 1) {
                el.textContent = token.value.replace(/ /g, '.');
            } else {
                el.textContent = token.value;
            }
        }
        
        // hide underlay elements that we aren't using
        while (i < underlay.els.length) {
            underlay.els[i++].style.display = 'none';
        }
        
        // re-attach underlay to DOM after tmp
        container.appendChild(underlay.el);
        
        // remember tokens
        tokens = ntokens.slice(0);
        
        editel.style.width = String((underlay.el.offsetWidth >= editel.clientWidth) ? underlay.el.offsetWidth : container.offsetWidth - 7 - ruler.offsetWidth) + 'px';
        editel.style.left = String(ruler.el.offsetWidth + ruler.padding) + 'px';
        underlay.el.style.top = editel.style.top;
        underlay.el.style.left = editel.style.left;
        
        scrollToCursor();
    };
    
    
    function editCount(key) {
        /*
         * Returns the buffered edit repeat count.
         */
        
        var re = new RegExp('.*?(\\d+)' + key + '$');
        return (re.test(editbuffer)) ? parseInt(editbuffer.replace(re, '$1'), 10) : 1;
    };
    
    
    function insertMode(insert) {
        /*
         * Sets the mode to insert if true.
         */
        
        if (insert) {
            cursor.el.style.background = 'none';
            cursor.el.style.opacity = '1';
            command.el.style.visibility = 'hidden';
            insertmode = true;
            editbuffer = '';
            return;
        }
        cursor.el.style.background = colours.cursorEdit;
        cursor.el.style.opacity = '.5';
        command.input.style.display = 'none';
        command.el.style.visibility = 'visible';
        insertmode = false;
        editbuffer = '';
    };
    
    
    function commandInput(input) {
        /*
         * Sets the state of the command-line input.
         */
        
        if (input) {
            command.input.style.display = 'inline';
            command.input.value = ':';
            command.input.focus();
            command.input.selectionStart = command.input.selectionEnd = 1;
            command.mode = true;
            return;
        }
        command.input.value = '';
        command.input.style.display = 'none';
        command.mode = false;
        editel.focus();
    };
    
    
    function execute(cmd) {
        /*
         * Executes the specified command string.
         */
        
        var goto_re = /:([0-9]+|\$)/;
        
        if (cmd === ':q') {
            window.open('', '_self', '');
            window.close();
        } else if (cmd === ':w') {
            save();
        } else if (cmd === ':wq') {
            save(function(m) {
                if (m.hasOwnProperty('success')) {
                    window.open('', '_self', '');
                    window.close();
                }
            });
        } else if (goto_re.test(cmd)) {
            var line = cmd.replace(goto_re, '$1');
            if (line === '$') {
                line = text.split('\n').length;
            } else {
                line = parseInt(line);
            }
            var offset = 0;
            while (offset != -1 && line > 1) {
                offset = text.indexOf('\n', offset) + 1;
                line--;
            }
            editel.selectionStart = editel.selectionEnd = offset;
            refreshCursor();
        }
    };
    
    
    function cursorToLineBegin() {
        /*
         * Moves to the cursor the first none space character of the line.
         */
        
        var pos = text.lastIndexOf('\n', editel.selectionStart - 1) + 1;
        while (text.charAt(pos) === ' ') {
            pos++;
        }
        cursor.indent = pos - text.lastIndexOf('\n', pos);
        return pos;
    };
    
    
    function cursorToLineEnd() {
        /*
         * Moves to the cursor the end of the line.
         */
        
        var pos = text.indexOf('\n', editel.selectionStart);
        if (pos === -1) pos = text.length;
        cursor.indent = pos - text.lastIndexOf('\n', pos - 1) - 1;
        return pos;
    };
    
    
    function keyevent(event) {
        /*
         * Receives keydown and keyup events from the editarea.
         */
        
        c = event.keyCode;
        pos = parseInt(editel.selectionStart, 10);
        
        if (insertmode) {
            if (c === 27) {
                insertMode(false);
            } else if (String(event.type) === 'keyup' || lastKeyType === 'keydown') {
                refreshDisplay();
            }
            lastKeyType = String(event.type);
            return true;
        }
        
        if (String(event.type) === 'keyup') {
            return false;
        }
        
        if (command.mode) {
            switch (c) {
                case 13: // ENTER execute command
                    execute(command.input.value);
                    commandInput(false);
                    return false;
                case 27: // ESC exit command-line input
                    commandInput(false);
                    break;
                default:
                    break;
            }
            return true;
        }
        
        switch (c) {
            case 52: // SHIFT+'4' ('$') cursor to end of line
                if (event.shiftKey) {
                    pos = cursorToLineEnd();
                } else {
                    editbuffer += '4';
                }
                break;
            case 54: // SHIFT+'6' ('^') cursor to first nonespace of line
                if (event.shiftKey) {
                    pos = cursorToLineBegin();
                } else {
                    editbuffer += '6';
                }
                break;
            case 59: // Firefox SHIFT+';' (':') enter command-line input
            case 186: // Chrome SHIFT+';' (':') enter command-line input
                if (event.shiftKey) {
                    commandInput(true);
                }
                return false;
            case 65: // 'a' append
                pos = (event.shiftKey) ? cursorToLineEnd() : pos + 1;
                insertMode(true);
                break;
            case 73: // 'i' insert
                pos = (event.shiftKey) ? cursorToLineBegin() : pos;
                insertMode(true);
                break;
            case 37: // cursor left
            case 72: // 'h' cursor left
                if (pos > 0) {
                    pos--;
                    cursor.indent--;
                }
                editbuffer = '';
                break;
            case 40: // cursor down
            case 74: // 'j' cursor down
                a = text.indexOf('\n', pos) + 1;
                b = text.indexOf('\n', a);
                if (a !== 0) {
                    a += cursor.indent;
                    pos = (a < b || b === -1) ? a : b;
                }
                editbuffer = '';
                break;
            case 38: // cursor up
            case 75: // 'k' cursor up
                if (pos - cursor.indent === 0) return false;
                a = text.lastIndexOf('\n', pos - 1);
                if (a !== -1) {
                    b = text.lastIndexOf('\n', a - 1) + 1;
                    pos = (a - b < cursor.indent) ? a : b + cursor.indent;
                }
                editbuffer = '';
                break;
            case 39: // cursor right
            case 76: // 'l' cursor right
                if (pos < text.length) {
                    pos++;
                    cursor.indent++;
                }
                editbuffer = '';
                break;
            case 68: // 'd' delete line if editbuffer ends with d
                if (/d$/g.test(editbuffer)) {
                    a = editCount('d');
                    while (a-- > 0) {
                        b = text.lastIndexOf('\n', pos - 1) + 1;
                        c = text.indexOf('\n', pos) + 1;
                        if (c === 0 && b > 0) b--;
                        editel.value = text.slice(0, b);
                        if (c > 0) editel.value += text.slice(c);
                        editel.selectionStart = editel.selectionEnd = b;
                        refreshDisplay();
                    }
                    editbuffer = '';
                } else {
                    editbuffer += 'd';
                }
                break;
            case 79: // 'o' line append or line insert if SHIFT+
                editbuffer += 'o';
                a = editCount('o');
                while (a-- > 0) {
                    if (event.shiftKey) {
                        pos = text.lastIndexOf('\n', pos - 1) + 1;
                    } else {
                        pos = text.indexOf('\n', pos) + 1;
                        if (pos === 0) pos = text.length + 1;
                    }
                    editel.value = text.slice(0, pos) + '\n' + text.slice(pos);
                    editel.selectionStart = editel.selectionEnd = pos;
                    refreshDisplay();
                }
                insertMode(true);
                break;
            case 88: // 'x' delete character if editbuffer ends with d
                editbuffer += 'x';
                a = editCount('x');
                while (a-- > 0) {
                    if (pos === text.length) pos--;
                    editel.value = text.slice(0, pos) + text.slice(pos + 1);
                    editel.selectionStart = editel.selectionEnd = pos;
                    refreshDisplay();
                }
                editbuffer = '';
                break;
            default:
                editbuffer += String.fromCharCode(c);
                break;
        }
        
        editel.selectionStart = editel.selectionEnd = pos;
        refreshCursor();
        return false;
    };
    
    // command bar
    command.el = element.create(container, 'div', 'ji_cmd_bar', [
        command.input = element.create('input', 'ji_cmd_input', {'type':'text'}),
        command.info = element.create('span', 'ji_cmd_info')
    ]);
    
    ruler.el = element.create('div', 'ji_ruler');
    ruler.tmp = element.create('div', 'ji_ruler_num');
    editel = element.create(container, 'textarea', 'ji_edit');
    underlay.el = element.create(container, 'span', 'ji_underlay');
    underlay.tmp = element.create('span', 'ji_underlay_tile');
    cursor.el = element.create(container, 'span', 'ji_cursor', [' ']);
    
    
    // allocate ruler number elements before attaching ruler to dom
    for (i = 0; i < 20000; i++) {
        el = ruler.el.appendChild(ruler.tmp.cloneNode(true));
        el.style.display = 'none';
        el.textContent = String(i + 1) + '\n';
        ruler.els.push(el);
    }
    
    // now attach the ruler element
    container.appendChild(ruler.el);
    
    
    element.create(document.head, 'style', {'type': 'text/css'}, [
        '.ji_cmd_bar {',
        '    position  : fixed;',
        '    background: ' + colours.cmdbarbg + ';',
        '    right     : 0px;',
        '    bottom    : 0px;',
        '    visibility: hidden;',
        '    z-index   : 2;',
        '}',
        '.ji_cmd_input {',
        '    display   : none;',
        '    background: none;',
        '    border    : none;',
        '    font      : ' + font.css + ';',
        '}',
        '.ji_cmd_info {',
        '    white-space: pre;',
        '    font       : ' + font.css + ';',
        '    padding    : 4px;',
        '}',
        '.ji_ruler {',
        '    white-space  : pre;',
        '    font         : ' + font.css + ';',
        '    color        : ' + colours.linenumber + ';',
        '    text-align   : right;',
        '    float        : left;',
        '    padding-left : 5px;',
        '    padding-right: 5px;',
        '    background   : ' + colours.ruler + ';',
        '    width        : 2em;',
        '    border-right : 1px solid ' + colours.lineborder + ';',
        '}',
        '.ji_ruler_num {',
        '    white-space: pre;',
        '    font       : ' + font.css + ';',
        '    color      : ' + colours.linenumber + ';',
        '}',
        '.ji_edit {',
        '    position  : absolute;',
        '    z-index   : 2;',
        '    top       : 0;',
        '    background: none;',
        '    color     : transparent;',
        '    border    : none;',
        '    outline   : none;',
        '    padding   : 0;',
        '    margin    : 0;',
        '    overflow  : hidden;',
        '    font      : ' + font.css + ';',
        '    resize    : none;',
        '}',
        '.ji_underlay {',
        '    white-space: pre;',
        '    font       : ' + font.css + ';',
        '    position   : absolute;',
        '    z-index    : 1;',
        '}',
        '.ji_underlay_tile {',
        '    white-space: pre;',
        '    font       : ' + font.css + ';',
        '}',
        '.ji_cursor {',
        '    white-space: pre;',
        '    position   : absolute;',
        '    z-index    : 3;',
        '    font       : ' + font.css + ';',
        '    border-left: 2px solid ' + colours.cursorInsert + ';',
        '}'
    ]);
    
    // container style
    container.style.position = 'relative';
    container.style.zIndex = '0';
    container.style.width = '100%';
    container.style.height = '100%';
    container.style.margin = '0';
    container.style.background = colours.background;
    container.style.overflow = 'auto';
    document.onclick = container.onclick = function(){editel.focus();};
    
    // refresh the display when clicked or window resize
    editel.onclick = window.onresize = refreshDisplay;
    
    // handle key events
    editel.onkeydown = editel.onkeyup = keyevent;
    command.input.onkeydown = editel.onkeyup = keyevent;
    
    font.style = window.getComputedStyle(cursor.el, null);
    font.width = parseFloat(font.style.getPropertyValue('width'));
    font.height = parseFloat(font.style.getPropertyValue('height'));
    
    // lastly refresh the display and focus textarea
    refreshDisplay();
    editel.focus();
    
    
    i = oj.args.length;
    while (i--) {
        if (oj.args[i][0] === 'f') open(oj.args[i][1]);
    }
    
});
